//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

public enum ResponseFileFormat: String, Equatable, Hashable, CaseIterable, Sendable, Codable, Serializable {
    case unescapedNewlineSeparated
    case unixShellQuotedNewlineSeparated
    case unixShellQuotedSpaceSeparated
    case windowsShellQuotedNewlineSeparated
    case llvmStyleEscaping
}

public enum ResponseFiles: Sendable {
    public static func responseFileContents(args: [String], format: ResponseFileFormat) -> String {
        switch format {
        case .unescapedNewlineSeparated:
            return args.map { $0 + "\n" }.joined()
        case .unixShellQuotedNewlineSeparated:
            let escaper = UNIXShellCommandCodec(encodingStrategy: .singleQuotes, encodingBehavior: .argumentsOnly)
            return args.map { escaper.encode([$0]) + "\n" }.joined()
        case .unixShellQuotedSpaceSeparated:
            return UNIXShellCommandCodec(encodingStrategy: .singleQuotes, encodingBehavior: .argumentsOnly).encode(args)
        case .windowsShellQuotedNewlineSeparated:
            let escaper = WindowsProcessArgumentsCodec()
            return args.map { escaper.encode([$0]) + "\r\n" }.joined()
        case .llvmStyleEscaping:
            return LLVMStyleCommandCodec().encode(args)
        }
    }

    // Adapted from SwiftDriver's response file support.
    public static func expandResponseFiles(_ args: [String], fileSystem: any FSProxy, relativeTo basePath: Path, format: ResponseFileFormat) throws -> [String] {
        var visited: Set<Path> = []
        return try expandResponseFiles(args, fileSystem: fileSystem, relativeTo: basePath, format: format, visitedResponseFiles: &visited)
    }

    private static func expandResponseFiles(_ args: [String], fileSystem: any FSProxy, relativeTo basePath: Path, format: ResponseFileFormat, visitedResponseFiles: inout Set<Path>) throws -> [String] {
        var result: [String] = []
        for arg in args {
            if arg.first == "@" {
                let responseFile = basePath.join(arg.dropFirst())
                // Guard against infinite parsing loop.
                guard visitedResponseFiles.insert(responseFile.normalize()).inserted else {
                    throw StubError.error("Attempted to recursively expand '\(responseFile.str)'")
                }
                defer {
                    visitedResponseFiles.remove(responseFile)
                }

                let contents = try fileSystem.read(responseFile).asString
                let tokens = tokenizeResponseFile(contents, format: format)
                result.append(contentsOf: try expandResponseFiles(tokens, fileSystem: fileSystem, relativeTo: basePath, format: format, visitedResponseFiles: &visitedResponseFiles))
            } else {
                result.append(arg)
            }
        }

        return result
    }

    private static func tokenizeResponseFile(_ content: String, format: ResponseFileFormat) -> [String] {
        switch format {
        case .unescapedNewlineSeparated:
            return content.split { $0 == "\n" || $0 == "\r\n" }.map { String($0) }
        case .unixShellQuotedNewlineSeparated, .unixShellQuotedSpaceSeparated:
            return content.split { $0 == "\n" || $0 == "\r\n" }
                .flatMap { tokenizeUnixShellQuotedResponseFileLine($0) }
        case .windowsShellQuotedNewlineSeparated:
            return content.split { $0 == "\n" || $0 == "\r\n" }
                .map { tokenizeWindowsShellQuotedResponseFileArg($0) }
        case .llvmStyleEscaping:
            return (try? LLVMStyleCommandCodec().decode(content)) ?? []
        }
    }

    private enum UnixTokenState {
      case normal, escaping, quoted
    }

    /// Tokenizes a response file line generated by `UNIXShellCommandCodec` using the `.singleQuotes` strategy.
    private static func tokenizeUnixShellQuotedResponseFileLine<S: StringProtocol>(_ line: S) -> [String] {
        // Support double dash comments only if they start at the beginning of a line.
        if line.hasPrefix("//") { return [] }

        var tokens: [String] = []
        var token: String = ""
        // Conservatively assume ~1 token per line.
        token.reserveCapacity(line.count)

        var state: UnixTokenState = .normal

        for char in line {
            if char == #"\"# && state == .normal {
                // Backslashes only escape outside of quoted text.
                state = .escaping
                continue
            }

            if state == .escaping {
                state = .normal
                token.append(char)
                continue
            }

            if char == "'" {
                // We specify `.singleQuotes` as the quoting strategy for `UNIXShellCommandCodec`. All other special
                // characters are escaped by quoting.
                if state == .quoted {
                    state = .normal
                } else {
                    state = .quoted
                }
                continue
            }

            if char.isWhitespace && state == .normal {
                // This is unquoted, unescaped whitespace, start a new token.
                if !token.isEmpty {
                    tokens.append(token)
                    token = ""
                }
                continue
            }

            token.append(char)
        }

        // Add the final token
        if !token.isEmpty {
            tokens.append(token)
        }

        return tokens
    }

    private enum WindowsTokenState {
      case normal, escaping
    }

    private static func tokenizeWindowsShellQuotedResponseFileArg<S: StringProtocol>(_ arg: S) -> String {
        guard arg.first == "\"" && arg.last == "\"" else {
            return String(arg)
        }
        var result = ""
        var state = WindowsTokenState.normal
        for char in arg.dropFirst().dropLast() {
            switch state {
            case .normal:
                if char == "\\" {
                    state = .escaping
                } else {
                    result.append(char)
                }
            case .escaping:
                result.append(char)
                state = .normal
            }
        }
        return result
    }
}
